package com.llamacorp.equate.test; import android.support.test.espresso.UiController; import android.support.test.espresso.ViewAction; import android.support.test.espresso.ViewInteraction; import android.support.test.espresso.matcher.BoundedMatcher; import android.support.test.espresso.matcher.ViewMatchers; import android.support.v4.view.ViewPager; import android.view.View; import android.view.ViewConfiguration; import android.widget.EditText; import android.widget.ListView; import com.llamacorp.equate.R; import com.llamacorp.equate.test.IdlingResource.ViewPagerIdlingResource; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import static android.support.test.espresso.Espresso.onData; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.action.ViewActions.longClick; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.core.deps.guava.base.Preconditions.checkArgument; import static android.support.test.espresso.matcher.ViewMatchers.assertThat; import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.isEnabled; import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.anything; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; /** * Set of utilities used to help perform Espresso tests */ public class EspressoTestUtils { public static void setUp(MyActivityTestRule activityTestRule) { // make sure Espresso hold long clicks for enough time // if this fails, make sure Settings -> Accessibility -> Touch & hold delay // is set to medium or long (for CircleCI) long timeEspressoHoldsKey = (long) (ViewConfiguration.getLongPressTimeout()); long buttonLongTimeout = activityTestRule.getActivity() .getResources().getInteger(R.integer.long_click_timeout_test); assertThat(timeEspressoHoldsKey, is(greaterThanOrEqualTo(buttonLongTimeout))); } public static ViewPagerIdlingResource getPagerIdle(MyActivityTestRule activityTestRule) { // register an idling resource that will wait until a page settles before // doing anything next (such as clicking a unit within it) ViewPager vp = (ViewPager) activityTestRule.getActivity() .findViewById(com.llamacorp.equate.R.id.unit_pager); return new ViewPagerIdlingResource(vp, "unit_pager"); } public static void assertResultPreviewInvisible() { onView(withId(R.id.resultPreview)).check(matches( withEffectiveVisibility(ViewMatchers.Visibility.GONE))); } public static void assertResultPreviewEquals(String expected) { onView(withId(R.id.resultPreview)).check(matches(allOf(isDisplayed(), withText(expected)))); } public static void assertExpressionEquals(String expected) { getTextDisplay().check(matches(expressionEquals(expected))); } /** * Method to check an expression contains the text given by the testString * parameter. This method also checks the test string isn't null and to turn * it into a Matcher<String>. * @param testString is the string to check the expression against * @return a Matcher<View> that can be used to turn into a View Interaction */ private static Matcher<View> expressionEquals(String testString) { // use precondition to fail fast when a test is creating an invalid matcher checkArgument(!(testString.equals(null))); return expressionEquals(is(testString)); } /** * Note that ideal the method below should implement a describeMismatch * method (as used by BaseMatcher), but this method is not invoked by * ViewAssertions.matches() and it won't get called. This means that I'm not * sure how to implement a custom error message. */ private static Matcher<View> expressionEquals(final Matcher<String> testString) { return new BoundedMatcher<View, EditText>(EditText.class) { @Override public void describeTo(Description description) { description.appendText("with expression text: " + testString); } @Override protected boolean matchesSafely(EditText item) { return testString.matches(item.getText().toString()); } }; } private static ViewInteraction getTextDisplay() { return onView(withId(R.id.textDisplay)); } /** * Clicks on the tab for the provided Unit Type name. Note that the Unit Type * doesn't need to be visible. */ public static void selectUnitTypeDirect(final String unitTypeName) { onView(allOf(withText(unitTypeName))).perform( new ViewAction() { @Override public Matcher<View> getConstraints() { return isEnabled(); // no constraints, they are checked above } @Override public String getDescription() { return "click unit type" + unitTypeName; } @Override public void perform(UiController uiController, View view) { view.performClick(); } } ); } /** * Clicks a unit in the unit pager with the displayed name of unitName */ public static void clickUnit(String unitName) { onView(allOf(anyOf(withText(unitName), withText("➜ " + unitName)), isDescendantOfA(withId(R.id.unit_pager)) , isDisplayed())).perform(click()); } /** * Checks that the unit button is visible for the given string */ public static void checkUnitButtonVisibleWithArrow(String buttonText) { checkUnitButtonVisible("➜ " + buttonText); } /** * Checks that the unit button is visible for the given string */ public static void checkUnitButtonVisible(String buttonText) { onView(allOf(withText(buttonText), isDescendantOfA(withId(R.id.unit_pager)))) .check(matches(isDisplayed())); } public static void clickButtons(String buttonString) { for (int i = 0; i < buttonString.length(); i++) { String s = buttonString.substring(i, i + 1); switch (s) { case "a": i++; clickPrevAnswer(Integer.parseInt(buttonString.substring(i, i + 1))); break; case "q": i++; clickPrevQuery(Integer.parseInt(buttonString.substring(i, i + 1))); break; default: clickButton(s); break; } } } private static void clickButton(String s) { int id; boolean longClick = false; //special case buttons switch (s) { case "^": id = R.id.multiply_button; longClick = true; break; case "E": id = R.id.percent_button; longClick = true; break; default: id = getButtonID(s); } if (longClick) onView(withId(id)).perform(longClick()); else onView(withId(id)).perform(click()); } /** * Helper function takes a string of a key hit and passes back the View id * * @param s plain text form of the button * @return id of the button */ private static int getButtonID(String s) { int[] numButtonIds = { R.id.zero_button, R.id.one_button, R.id.two_button, R.id.three_button, R.id.four_button, R.id.five_button, R.id.six_button, R.id.seven_button, R.id.eight_button, R.id.nine_button}; int buttonId = -1; switch (s) { case "+": buttonId = R.id.plus_button; break; case "-": buttonId = R.id.minus_button; break; case "*": buttonId = R.id.multiply_button; break; case "/": buttonId = R.id.divide_button; break; case ".": buttonId = R.id.decimal_button; break; case "=": buttonId = R.id.equals_button; break; case "%": buttonId = R.id.percent_button; break; case "^": buttonId = R.id.multiply_button; break; case "(": buttonId = R.id.open_para_button; break; case ")": buttonId = R.id.close_para_button; break; case "b": buttonId = R.id.backspace_button; break; case "C": buttonId = R.id.clear_button; break; default: //this for loop checks for numerical values for (int i = 0; i < 10; i++) if (s.equals(Character.toString((char) (48 + i)))) buttonId = numButtonIds[i]; } if (buttonId == -1) throw new InvalidButtonViewException( "No View could be found for button = \"" + s + "\""); return buttonId; } public static void clickPrevAnswer() { clickPrevAnswer(0); } public static void clickPrevAnswer(int position) { ResultClicker rc = new ResultClicker(); rc.clickPrevAnswer(position); } public static void clickPrevQuery() { clickPrevQuery(0); } public static void clickPrevQuery(int position) { ResultClicker rc = new ResultClicker(); rc.clickPrevQuery(position); } private static class ResultClicker { private int numberOfAdapterItems; public void clickPrevAnswer(int position) { updateNumberofResults(); onData(anything()) .inAdapterView(withId(android.R.id.list)) .atPosition(numberOfAdapterItems - 1 - position) .onChildView(withId(R.id.list_item_result_textPrevAnswer)) .perform(click()); } public void clickPrevQuery(int position) { updateNumberofResults(); onData(anything()) .inAdapterView(withId(android.R.id.list)) .atPosition(numberOfAdapterItems - 1 - position) .onChildView(withId(R.id.list_item_result_textPrevQuery)) .perform(click()); } private void updateNumberofResults() { onView(withId(android.R.id.list)).check(matches(new TypeSafeMatcher<View>() { @Override public boolean matchesSafely(View view) { ListView listView = (ListView) view; //here we assume the adapter has been fully loaded already numberOfAdapterItems = listView.getAdapter().getCount(); return true; } @Override public void describeTo(Description description) { } })); } } private static class InvalidButtonViewException extends RuntimeException { InvalidButtonViewException(String message) { super(message); } } }